package com.yanyeori.framework.datasource.config;

import com.yanyeori.framework.core.util.BeanUtil;
import com.yanyeori.framework.core.util.StringUtil;
import com.yanyeori.framework.datasource.aspect.DataSourceAspect;
import com.yanyeori.framework.datasource.builder.DataSourceBuilder;
import com.yanyeori.framework.datasource.builder.DataSourceBuilderFactory;
import com.yanyeori.framework.datasource.builder.DefaultDataSourceBuilder;
import com.yanyeori.framework.datasource.configprop.DataSourceConfigProps;
import com.yanyeori.framework.datasource.configprop.MultiDataSourceConfigProps;
import com.yanyeori.framework.datasource.mybatisplus.config.MybatisPlusConfig;
import com.yanyeori.framework.datasource.strategy.ListPollStrategy;
import com.yanyeori.framework.datasource.strategy.ListStrategy;
import com.yanyeori.framework.datasource.switcher.SurviveEviction;
import com.yanyeori.framework.datasource.switcher.SurviveSwitcher;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 数据源自动配置
 * 默认支持连接池：
 * com.alibaba.druid.pool.DruidDataSource
 * com.zaxxer.hikari.HikariDataSource
 *
 * @author chenkuan 2020/8/6
 */
@Slf4j
@Configuration
@ConditionalOnClass({DataSource.class})
@EnableConfigurationProperties({DataSourceConfigProps.class})
@AutoConfigureBefore({DataSourceAutoConfiguration.class})
@Import({DataSourceBuilderFactory.class, MybatisPlusConfig.class})
public class DataSourceAutoConfig {

    @Bean
    public DynamicDataSource dynamicDataSource(DataSourceConfigProps props, DataSourceBuilderFactory factory) {
        log.debug("Start registering dynamic datasource: [spring.datasource.type={}]", props.getType());
        //数据源map
        Map<Object, Object> dataSourceMap = new HashMap<>();
        //主从关系map
        Map<String, List<String>> primarySlaveDbMap = new HashMap<>();
        //配置的数据源名称，用于打印
        StringBuilder stringBuilder = new StringBuilder();

        //默认数据源
        DataSource masterDataSource = getDataSource(factory, props, null);
        dataSourceMap.put(props.getName(), masterDataSource);
        stringBuilder.append(props.getName());

        //辅数据源
        List<MultiDataSourceConfigProps> multiDataSources = props.getList();
        if (CollectionUtils.isNotEmpty(multiDataSources)) {
            //获取辅数据源
            for (MultiDataSourceConfigProps slaveProps : multiDataSources) {
                DataSource slaveDataSource = getDataSource(factory, props, slaveProps);
                dataSourceMap.put(slaveProps.getName(), slaveDataSource);
                stringBuilder.append(",").append(slaveProps.getName());
            }
            //处理主从关系
            primarySlaveDbMap.putAll(handlePrimarySlaveDb(multiDataSources));
        }

        //动态数据源单例
        DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
        //注入动态数据源
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        //注入主从关系
        dynamicDataSource.setPrimarySlaveDbMap(primarySlaveDbMap);
        //初始化数据源
        dynamicDataSource.afterPropertiesSet();

        log.info("Dynamic datasource registered successfully: [{}]", stringBuilder);
        return dynamicDataSource;
    }

    /**
     * 根据spring.datasource.type选择数据源
     */
    private DataSource getDataSource(DataSourceBuilderFactory factory, DataSourceConfigProps props, MultiDataSourceConfigProps basicProps) {
        if (basicProps == null) {
            basicProps = new MultiDataSourceConfigProps();
            BeanUtil.copyProperties(props, basicProps);
        }
        //url添加serverTimezone时区
        addTimeZone(props, basicProps);
        //构建DataSource
        DataSourceBuilder dataSourceBuilder = factory.get(props.getType());
        if (dataSourceBuilder == null) {
            log.warn("Unsupported data source type: spring.datasource.type={}, use DefaultDataSourceBuilder", props.getType());
            dataSourceBuilder = new DefaultDataSourceBuilder();
        }
        return dataSourceBuilder.build(props, basicProps);
    }

    /**
     * url添加serverTimezone时区
     *
     * @param basicProps 数据库连接属性
     */
    private void addTimeZone(DataSourceConfigProps props, MultiDataSourceConfigProps basicProps) {
        String serverTimezone = "serverTimezone=";
        String url = basicProps.getUrl();
        String timeZone = props.getConfig().getTimeZone();
        if (url.toLowerCase().contains(serverTimezone.toLowerCase()) || StringUtil.isEmpty(timeZone)) {
            return;
        }
        StringBuilder newUrl = new StringBuilder(url);
        newUrl.append(url.contains("?") ? "&" : "?");
        newUrl.append(serverTimezone).append(timeZone);
        props.setUrl(newUrl.toString());
        basicProps.setUrl(newUrl.toString());
    }

    /**
     * 根据辅库维护主从数据源关系
     *
     * @param mutiDataSources 辅库数据源配置
     */
    private Map<String, List<String>> handlePrimarySlaveDb(List<MultiDataSourceConfigProps> mutiDataSources) {
        Map<String, List<String>> map = new HashMap<>();
        for (MultiDataSourceConfigProps dataSource : mutiDataSources) {
            if (StringUtil.isNotBlank(dataSource.getPrimaryDb())) {
                map.computeIfAbsent(dataSource.getPrimaryDb(), k -> new ArrayList<>()).add(dataSource.getName());
            }
        }
        return map;
    }

    @Bean
    public SurviveEviction surviveEviction(JdbcTemplate jdbcTemplate, DataSourceConfigProps dataSourceConfigProps) {
        return new SurviveEviction(jdbcTemplate, dataSourceConfigProps);
    }

    @Bean
    public SurviveSwitcher surviveSwitcher(SurviveEviction surviveEviction, DataSourceConfigProps dataSourceConfigProps) {
        return new SurviveSwitcher(surviveEviction, dataSourceConfigProps);
    }

    @Bean
    public DataSourceAspect dataSourceAspect(SurviveSwitcher surviveSwitcher, DataSourceConfigProps dataSourceConfigProps) {
        //默认轮询策略
        ListStrategy strategy = new ListPollStrategy();
        String slaveDbListStrategy = dataSourceConfigProps.getConfig().getSlaveDbListStrategy();
        if (StringUtil.isNotEmpty(slaveDbListStrategy)) {
            try {
                strategy = (ListStrategy) Class.forName(slaveDbListStrategy).newInstance();
            } catch (Exception e) {
                // e.printStackTrace();
            }
        }
        return new DataSourceAspect(surviveSwitcher, dataSourceConfigProps, strategy);
    }

    /**
     * 自动映射Map<String, Object>到sql输入参数：
     * String sql = "select * from t_reconciliation_summary where nostro_account in (:nostroAccountList) and value_date in (:valueDateList)";
     * Map<String, Object> paramMap = new HashMap<>();
     * paramMap.put("nostroAccountList", nostroAccountList);
     * paramMap.put("valueDateList", valueDateList);
     * List<ReconciliationSummary> list = namedParameterJdbcTemplate.query(sql, paramMap, new BeanPropertyRowMapper<>(ReconciliationSummary.class));
     */
    @Bean
    @ConditionalOnMissingBean(NamedParameterJdbcTemplate.class)
    public NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcTemplate jdbcTemplate) {
        return new NamedParameterJdbcTemplate(jdbcTemplate);
    }

    @Bean
    public TimeZoneCustomizer timeZoneCustomizer(DataSourceConfigProps dataSourceConfigProps) {
        return new TimeZoneCustomizer(dataSourceConfigProps.getConfig().getTimeZone());
    }
}
